﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Tavis.Interfaces;

namespace Tavis {
    public class RestAgent : IDisposable {

        public event Action AgentTerminated = delegate { };
        public ISynchronizeInvoke SynchronizationObject { private get; set; }


        private readonly HttpClient _HttpClient;
        private readonly ILinkFactory _LinkFactory;
        private readonly SemanticsRegistry _SemanticsRegistry;
        private readonly Dictionary<string, HttpContent> _ContentStore = new Dictionary<string, HttpContent>();
        private readonly Dictionary<string, ILink> _Bookmarks = new Dictionary<string, ILink>();
        private readonly Link _RootLink;
        private readonly LinkHistory _LinkHistory = new LinkHistory();
        private readonly object _SyncRoot = new object();
        private readonly object _Navigating = new object();
        private readonly HashSet<Task<HttpResponseMessage>> _PendingRequests = new HashSet<Task<HttpResponseMessage>>();
        private readonly List<ResponseHandler> _ResponseActions = new List<ResponseHandler>();

        private RestAgentRequestState _CurrentRequestState;
        private RestAgent _ChildAgent;

        private HypermediaContent _CurrentContent;
        private HttpStatusCode _CurrentStatusCode = HttpStatusCode.OK;

        private Action<IResponseReceivedEventArgs> _DefaultResponseStartHandler;
        private Action<IHypermediaContent> _DefaultResponseCompleteHandler;
        private Action<Exception> _DefaultErrorHandler;


        public ISemanticsRegistry SemanticsRegistry {
            get { return _SemanticsRegistry; }
        }

        public RestAgent(Uri rootUrl)
            : this(CreateHttpClient(), CreateSemanticsRegistry()) {
            _RootLink = new Link(rootUrl);
            _HttpClient.BaseAddress = _RootLink.Target;
            NavigateTo(_RootLink);
        }

        public RestAgent()
            : this(CreateHttpClient(), CreateSemanticsRegistry()) {
        }

        public RestAgent(HttpClient httpClient)
            : this(httpClient, CreateSemanticsRegistry()) {
        }


        private RestAgent(HttpClient httpClient, SemanticsRegistry semanticsRegistry) {
            CurrentRequestState = RestAgentRequestState.Idle;
            _LinkFactory = semanticsRegistry.LinkFactory;
            _SemanticsRegistry = semanticsRegistry;
            _HttpClient = httpClient;
            _SemanticsRegistry = semanticsRegistry;

        }

        public RestAgentRequestState CurrentRequestState {
            get {
                lock (_SyncRoot) {
                    return _CurrentRequestState;
                }
            }
            private set {
                lock (_SyncRoot) {
                    _CurrentRequestState = value;
                    Debug.WriteLine("Changing state to : " + _CurrentRequestState);
                }
            }
        }



        public HttpRequestHeaders DefaultRequestHeaders {
            get { return _HttpClient.DefaultRequestHeaders; }
        }


        public HttpStatusCode CurrentStatusCode {
            get { return _CurrentStatusCode; }
        }

        public IHypermediaContent CurrentContent {
            get {
                WaitUntilIdle();

                lock (_Navigating) {
                    return _CurrentContent;
                }
            }
        }

        public ILink CurrentLocation {
            get { return _LinkHistory.CurrentLocation; }
        }

        public bool CanNavigatePrevious {
            get { return _LinkHistory.PreviousLocation != null; }
        }


        public void RegisterSemanticsProvider(ISemanticsProvider provider) {
            provider.RegisterSemantics(_SemanticsRegistry);
        }

        public void RegisterResponseAction(ResponseHandler responseHandler) {
            _ResponseActions.Add(responseHandler);
        }

        private Task<HttpContent> GetContent(ILink link) {
            if (link == null || link.Target == null) return null;
            link.MakeAbsolute(CurrentLocation);

            var content = _SemanticsRegistry.GetPreregisteredContent(link);
            if (content != null) {
                return Task.Factory.StartNew(() => content) ;
            }

            return  _HttpClient.GetAsync(link.Target)
                .ContinueWith((t) =>
                                  {
                                      var response = t.Result;
                                      if (response.StatusCode == HttpStatusCode.OK)
                                      {
                                          return response.Content;
                                      }
                                      else
                                      {
                                          Debug.WriteLine(
                                              "Error retrieving content from link with URI: " +
                                              link.Target + "  Status: " +
                                              response.StatusCode);
                                          return null;
                                      }
                                  });
        }


        private Task<HttpContent> GetContentAsync(ILink link) {
            if (link == null || link.Target == null) return null;
            link.MakeAbsolute(CurrentLocation);

            var content = _SemanticsRegistry.GetPreregisteredContent(link);
            if (content != null) {
                return Task.Factory.StartNew(() => content);
            }

            return _HttpClient.GetAsync(link.Target)
                .ContinueWith(task => {
                    var response = task.Result;

                    if (response.StatusCode == HttpStatusCode.OK) {
                        return response.Content;
                    } else {
                        Debug.WriteLine("Error retrieving content from link with URI: " + link.Target + "  Status: " + response.StatusCode);
                        return null;
                    }
                }
            );
        }


        public HttpContent GetContent(string label) {
            return _ContentStore[label];
        }


        public void RegisterDefaultResponseStartHandler(Action<IResponseReceivedEventArgs> callback) {
            _DefaultResponseStartHandler = callback;
        }

        public void RegisterDefaultResponseCompleteHandler(Action<IHypermediaContent> callback) {
            _DefaultResponseCompleteHandler = callback;
        }

        public void RegisterDefaultErrorHandler(Action<Exception> callback) {
            _DefaultErrorHandler = callback;
        }


        public void CreateBookmark(string bookmark, ILink link) {
            _Bookmarks.Add(bookmark, link);
        }

        public ILink RetreiveBookmark(string bookmarkName) {
            return _SemanticsRegistry.GetBookmark(bookmarkName) ?? _Bookmarks[bookmarkName];
        }


        public Task EmbedContent(string label, ILink link) {
            return GetContent(link).ContinueWith((t)=>_ContentStore[label] = t.Result);
        }

        public Task[] LoadAllEmbeddedResources() {
            var tasks = new List<Task<HttpContent>>();

            foreach (var link in CurrentContent.GetLinks().Where(l => l.StateChange == StateChangeType.Embed)) {
                var label = link.Target.OriginalString;
                var task = GetContentAsync(link);
                task.ContinueWith(t => _ContentStore[label] = t.Result);

                tasks.Add(task);
            }

            return tasks.ToArray();
        }




        public void NavigateTo(ILink link,
                                Action<IResponseReceivedEventArgs> onResponseStart = null, 
                                Action<IHypermediaContent> onComplete = null, 
                                Action<Exception> onError = null) {
            if (link.StateChange == StateChangeType.Independent) {
                NavigateToIndependantLink(link, onResponseStart, onComplete, onError);
            } else {
                InternalNavigateTo(link, onResponseStart, onComplete, onError);
            }
        }

        private void NavigateToIndependantLink(ILink link, Action<IResponseReceivedEventArgs> onResponseStart, Action<IHypermediaContent> onComplete, Action<Exception> onError) {
            _ChildAgent = new RestAgent(_HttpClient, _SemanticsRegistry);
            _ChildAgent._DefaultResponseCompleteHandler = _DefaultResponseCompleteHandler;
            _ChildAgent._DefaultResponseStartHandler = _DefaultResponseStartHandler;
            _ChildAgent._DefaultErrorHandler = _DefaultErrorHandler;

            _ChildAgent.InternalNavigateTo(link, onResponseStart, onComplete, onError);
            _ChildAgent.AgentTerminated += () => _ChildAgent = null;
        }


        public void NavigateToPrevious(Action<IResponseReceivedEventArgs> onResponseStart = null, Action<IHypermediaContent> onComplete = null, Action<Exception> onError = null) {
            if (!CanNavigatePrevious) {
                throw new InvalidOperationException("Nowhere to go back to");
            }

            var link = _LinkHistory.PreviousLocation;

            InternalNavigateTo(link, onResponseStart, onComplete, onError);
        }


        private void InternalNavigateTo(ILink link, Action<IResponseReceivedEventArgs> onResponseStart, Action<IHypermediaContent> onComplete, Action<Exception> onError) {
            if (link.StateChange == StateChangeType.Embed) throw new ArgumentException("Cannot Navigate a content link");

            lock (_Navigating) {
                // Forget about any pending requests (they'll complete silently in the background)
                if (_CurrentContent != null) {
                    _CurrentContent.SuppressValueLoadedEvent();
                }

                _PendingRequests.Clear();
            }

            onResponseStart = onResponseStart ?? _DefaultResponseStartHandler;
            onComplete = onComplete ?? _DefaultResponseCompleteHandler;
            onError = onError ?? _DefaultErrorHandler;

            CurrentRequestState = RestAgentRequestState.RequestPending;
            try {
                link.MakeAbsolute(CurrentLocation);

                var savedState = SaveState();

                Action<Exception> errorHandler = ex => {
                    RestoreState(savedState);
                    CurrentRequestState = RestAgentRequestState.Idle;
                    FireOnError(onError, ex);
                };

            
                Follow(link, response => HandleResponse(response, link, onResponseStart, onComplete, errorHandler), errorHandler);
            
            } catch {
                CurrentRequestState = RestAgentRequestState.Idle;
                throw;
            }

        }


        private void HandleResponse(HttpResponseMessage response, 
                                        ILink link, 
                                        Action<IResponseReceivedEventArgs> onResponseStart, 
                                        Action<IHypermediaContent> onComplete, 
                                        Action<Exception> errorHandler) {
            try {
                if (link.StateChange != StateChangeType.Replace) {
                    _LinkHistory.AddToHistory(link);
                }

                _CurrentStatusCode = response.StatusCode;

                FireOnResponseStart(link, response, onResponseStart);

                var content = response.Content;
                if (content != null) {
                    lock (_Navigating) {
                        _CurrentContent = new HypermediaContent(_SemanticsRegistry, response.Content, _LinkFactory, response.Headers, hContent => FireOnComplete(onComplete, hContent));
                    }
                    CurrentRequestState = RestAgentRequestState.Idle;
                } else {
                    lock (_Navigating) {
                        _CurrentContent = null;  // Link headers / Location could contain links.  Need an Empty HypermediaContent object
                    }
                    CurrentRequestState = RestAgentRequestState.Idle;

                    FireOnComplete(onComplete, null);
                }
            } catch (Exception ex) {
                errorHandler(ex);
            }
        }

        private void FireOnComplete(Action<IHypermediaContent> onComplete, IHypermediaContent content) {
            if (onComplete == null) return;

            if (SynchronizationObject != null) {
                SynchronizationObject.BeginInvoke(onComplete, new[] { content });
            } else {
                Task.Factory.StartNew(() => onComplete(content));
            }
        }

        private void FireOnResponseStart(ILink sourceLink, HttpResponseMessage response, Action<IResponseReceivedEventArgs> onResponseStart) {
            if (onResponseStart == null) return;

            var args = new ResponseReceivedEventArgs {
                ResponseMessage = response,
                SourceLink = sourceLink
            };

            if (SynchronizationObject != null) {
                SynchronizationObject.BeginInvoke(onResponseStart, new[] { args });
            } else {
                Task.Factory.StartNew(() => onResponseStart(args));
            }
        }

        private void FireOnError(Action<Exception> onError, Exception ex) {
            if (onError == null) return;

            if (SynchronizationObject != null) {
                SynchronizationObject.BeginInvoke(onError, new[] { ex });
            } else {
                Task.Factory.StartNew(() => onError(ex));
            }
        }


        private void Follow(ILink link, Action<HttpResponseMessage> callback, Action<Exception> onError) {
            var content = _SemanticsRegistry.GetPreregisteredContent(link);
            if (content != null) {
                var response = new HttpResponseMessage(HttpStatusCode.OK) {
                    Content = content
                };
                callback(response);
            } else {
                SendHttpRequest(link, callback, onError);
            }
        }


        private void SendHttpRequest(ILink link, Action<HttpResponseMessage> callback, Action<Exception> onError) {
            HttpRequestMessage request = CreateHttpRequestMessage(link);

            Task<HttpResponseMessage> task;
            lock (_Navigating) {
                task = _HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
                _PendingRequests.Add(task);
            }

            task.ContinueWith(t => {
                if (t.Status == TaskStatus.Faulted) {
                    onError(t.Exception);
                    return;
                }

                HandleFollowResponse(t, link, callback,onError);

            });
        }


        
        private void HandleFollowResponse(Task<HttpResponseMessage> task,
                                            ILink link,
                                            Action<HttpResponseMessage> callback,
                                            Action<Exception> onError         
            ){

            var response = task.Result;
            if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod) {
                Follow(new Link(response.Headers.Location), callback, onError);
                return;
            }

            if (response.StatusCode == HttpStatusCode.RedirectKeepVerb) {
                var redirectLink = new Link(response.Headers.Location);
                redirectLink.Method = link.Method;
                Follow(redirectLink, callback, onError);
                return;
            }

            var ignoreCallback = true;
            lock (_Navigating) {
                if (_PendingRequests.Contains(task)) {
                    _PendingRequests.Remove(task);
                    ignoreCallback = false;
                }
            }

            if (!ignoreCallback) {
                callback(response);
            }
        }

        private static HttpRequestMessage CreateHttpRequestMessage(ILink link) {
            var request = new HttpRequestMessage();

            var headers = request.Headers;
            if (!string.IsNullOrEmpty(link.Accept)) {
                headers.Accept.Add(new MediaTypeWithQualityHeaderValue(link.Accept));
            }
            request.Method = link.Method;
            request.RequestUri = link.Target;
            request.Content = link.Content;
            return request;
        }

        private static HttpClient CreateHttpClient() {
            var httpClient = new HttpClient(new RestAgentHandler());
            httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue("HttpService", "1.0")));
            return httpClient;
        }

        private void CreateDefaultResponseActions() {
            
        }

        private static SemanticsRegistry CreateSemanticsRegistry() {
            return new SemanticsRegistry(new LinkFactory());
        }

        private void WaitUntilIdle() {
            while (CurrentRequestState != RestAgentRequestState.Idle) {
                Thread.Sleep(1);
            }
        }


        public void Dispose() {
            if (_ChildAgent != null) {
                _ChildAgent.Dispose();
            }

            AgentTerminated();

            _HttpClient.Dispose();
        }

        private RestAgentState SaveState() {
            RestAgentState state;
            lock (_Navigating) {
                state = new RestAgentState(_CurrentContent, _CurrentStatusCode, CurrentLocation);
            }
            return state;
        }

        private void RestoreState(RestAgentState restAgentState) {
            lock (_Navigating) {
                _CurrentContent = restAgentState.Content;
                _CurrentStatusCode = restAgentState.StatusCode;
            }


            if (restAgentState.Location != _LinkHistory.CurrentLocation) {
                _LinkHistory.GoBack();
            }
        }

        private class RestAgentState {
            private readonly HypermediaContent _Content;
            private readonly HttpStatusCode _StatusCode;
            private readonly ILink _Location;

            public RestAgentState(HypermediaContent content, HttpStatusCode statusCode, ILink location) {
                _Content = content;
                _StatusCode = statusCode;
                _Location = location;
            }

            public ILink Location {
                get { return _Location; }
            }

            public HttpStatusCode StatusCode {
                get { return _StatusCode; }
            }

            public HypermediaContent Content {
                get { return _Content; }
            }
        }

        

    }

    public abstract class ResponseHandler {

        protected abstract bool ShouldRespond(HttpResponseMessage responseMessage);
        protected abstract void Handler(HttpResponseMessage responseMessage);

        public void Process(HttpResponseMessage responseMessage) {
            if (ShouldRespond(responseMessage)) {
                Handler(responseMessage);
            }
        }
    }

    public class GetRedirectResponse : ResponseHandler {
        protected override bool ShouldRespond(HttpResponseMessage response) {
            return response.StatusCode == HttpStatusCode.Redirect ||
                   response.StatusCode == HttpStatusCode.RedirectMethod;
        }
        protected override void Handler(HttpResponseMessage responseMessage) {
    //        Follow(new Link(response.Headers.Location), callback, onError);
            
        }
    }

}
